// Copyright 2019 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package session

import (
	"context"
	"fmt"
	"log"
	"mi/compiler/ast"
	"mi/compiler/diagnostic"
	"mi/compiler/parser"
	"mi/compiler/program"
	"mi/internal/event"
	"mi/internal/system"
	"mi/internal/xpath"
	"path/filepath"
	"strings"

	errors "mi/internal/xerrors"
	"sync"
)

type session struct {
	cache *Cache
	id    string

	optionsMu sync.Mutex
	options   *Options

	viewMu  sync.Mutex

	overlayMu sync.Mutex
	overlays  map[xpath.URI]*overlay

	program program.Program

	// diagnostic hash cache
	diagnosticHash map[xpath.URI]string

	programMux sync.Mutex

	cacheProjectFiles map[string]*ast.ProjectFile
	cachePackages     map[string]*ast.Package
	cacheSourceFiles  map[string]*ast.SourceFile
}

type overlay struct {
	session *session
	uri     xpath.URI
	text    []byte
	hash    string
	version float64
	kind    FileKind

	// saved is true if a file matches the state on disk,
	// and therefore does not need to be part of the overlay sent to go/packages.
	saved bool
}

func (o *overlay) Read() ([]byte, error) {
	return o.text, nil
}

func (o *overlay) FileIdentity() FileIdentity {
	return FileIdentity{
		URI:  o.uri,
		Hash: o.hash,
		Kind: o.kind,
	}
}

func (o *overlay) VersionedFileIdentity() VersionedFileIdentity {
	return VersionedFileIdentity{
		URI:       o.uri,
		SessionID: o.session.id,
		Version:   o.version,
	}
}

func (o *overlay) Kind() FileKind {
	return o.kind
}

func (o *overlay) URI() xpath.URI {
	return o.uri
}

func (o *overlay) Version() float64 {
	return o.version
}

func (o *overlay) Session() string {
	return o.session.id
}

func (o *overlay) Saved() bool {
	return o.saved
}

// closedFile implements LSPFile for a file that the editor hasn't told us about.
type closedFile struct {
	FileHandle
}

func (c *closedFile) VersionedFileIdentity() VersionedFileIdentity {
	return VersionedFileIdentity{
		URI:       c.FileHandle.URI(),
		SessionID: "",
		Version:   0,
	}
}

func (c *closedFile) Saved() bool {
	return true
}

func (c *closedFile) Session() string {
	return ""
}

func (c *closedFile) Version() float64 {
	return 0
}

func (s *session) ID() string  { return s.id }

func (s *session) String() string { return s.id }

func (s *session) Options() *Options {
	s.optionsMu.Lock()
	defer s.optionsMu.Unlock()
	return s.options
}

func (s *session) SetOptions(options *Options) {
	s.optionsMu.Lock()
	defer s.optionsMu.Unlock()
	s.options = options
}

func (s *session) Shutdown(ctx context.Context) {
	s.viewMu.Lock()
	defer s.viewMu.Unlock()
	event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s))
}

func (s *session) Cache() interface{} {
	return s.cache
}

func (s *session) ModifyFiles(ctx context.Context, changes []FileModification) error {
	return s.DidModifyFiles(ctx, changes)
}

func (s *session) getSourceFiles() []string {
	var result []string
	for uri, _ := range s.overlays {
		result = append(result, uri.Filename())
	}
	return result
}

func (s *session) getProjectFile(fileName string) string {
	// Look up
	var dir = filepath.Dir(fileName)

	// e.g
	// FileName is c:/a/b/c/main.mi
	// we need search for c:/a/b
	// So climb another layer
	dir = filepath.Dir(dir)

	var pre = ""
	for pre != dir {
		var projectFilePath = filepath.Join(dir, "项目.mip")
		exists, _ := xpath.PathExists(projectFilePath)
		if exists && strings.Index(fileName, filepath.Join(dir, "源码")) != -1 {

			return projectFilePath
		}
		pre = dir
		dir = filepath.Dir(dir)
	}

	return ""
}

func (s *session) DidModifyFiles(ctx context.Context, changes []FileModification) error {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	_, err := s.updateOverlays(ctx, changes)
	if err != nil {
		return err
	}

	// Cache source files
	if s.program != nil {
		s.cacheSourceFiles = make(map[string]*ast.SourceFile)
		var hasChange = func (uri xpath.URI) bool {
			for _, change := range changes {
				if xpath.CompareURI(change.URI, uri) == 0 {
					return true
				}
			}
			return false
		}

		var changePackages []*ast.Package
		for _, sourceFile := range s.program.GetSourceFiles() {
			if hasChange(sourceFile.Uri) {
				changePackages = append(changePackages, sourceFile.Parent().(*ast.Package))
				continue
			}
			s.cacheSourceFiles[sourceFile.Uri.Filename()] = sourceFile
		}

		var pkgHasChange = func(pkg *ast.Package) bool {
			for _, changePackage := range changePackages {
				if pkg == changePackage {
					return true
				}
			}
			return false
		}
		s.cachePackages = make(map[string]*ast.Package)
		for _, pkg := range s.program.GetPackages() {
			if pkgHasChange(pkg) {
				continue
			}

			s.cachePackages[pkg.DiskPath.Filename()] = pkg
		}
	}

	var contain = func(arr []xpath.URI, uri xpath.URI) bool {
		for _, x := range arr {
			if xpath.CompareURI(x, uri) == 0 {
				return true
			}
		}
		return false
	}
	var projectFiles []xpath.URI
	for uri, o := range s.overlays {
		if o.kind == Mi {
			var _path = s.getProjectFile(uri.Filename())
			if len(_path) > 0 && !contain(projectFiles, xpath.URIFromPath(_path)) {
				projectFiles = append(projectFiles, xpath.URIFromPath(_path))
			}
		}
	}

	var start = system.CurrentMillisecond()
	var host = createCompilerHost(s, ctx)
	s.program = program.CreateProgram(projectFiles, host)
	// Gets a type checker that can be used to
	// semantically analyze source files in the program.
	s.program.GetTypeChecker()
	log.Printf("updateProgram: %d\n", system.CurrentMillisecond() - start)
	return nil
}

func (s *session) updateOverlays(ctx context.Context, changes []FileModification) (map[xpath.URI]*overlay, error) {
	s.overlayMu.Lock()
	defer s.overlayMu.Unlock()

	for _, c := range changes {
		// Don't update overlays for metadata invalidations.
		if c.Action == InvalidateMetadata {
			continue
		}

		o, ok := s.overlays[c.URI]

		// If the file is not opened in an overlay and the change is on disk,
		// there's no need to update an overlay. If there is an overlay, we
		// may need to update the overlay's saved value.
		if !ok && c.OnDisk {
			continue
		}

		// Determine the file kind on open, otherwise, assume it has been cached.
		var kind FileKind
		switch c.Action {
		case Open:
			kind = DetectLanguage(c.LanguageID, c.URI.Filename())
		default:
			if !ok {
				return nil, errors.Errorf("updateOverlays: modifying unopened overlay %v", c.URI)
			}
			kind = o.kind
		}
		if kind == UnknownKind {
			return nil, errors.Errorf("updateOverlays: unknown file kind for %s", c.URI)
		}

		// Closing a file just deletes its overlay.
		if c.Action == Close {
			delete(s.overlays, c.URI)
			continue
		}

		// If the file is on disk, check if its content is the same as in the
		// overlay. Saves and on-disk file changes don't come with the file's
		// content.
		text := c.Text
		if text == nil && (c.Action == Save || c.OnDisk) {
			if !ok {
				return nil, fmt.Errorf("no known content for overlay for %s", c.Action)
			}
			text = o.text
		}
		// On-disk changes don't come with versions.
		version := c.Version
		if c.OnDisk {
			version = o.version
		}
		hash := hashContents(text)
		var sameContentOnDisk bool
		switch c.Action {
		case Delete:
			// Do nothing. sameContentOnDisk should be false.
		case Save:
			// Make sure the version and content (if present) is the same.
			if o.version != version {
				return nil, errors.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version)
			}
			if c.Text != nil && o.hash != hash {
				return nil, errors.Errorf("updateOverlays: overlay %s changed on save", c.URI)
			}
			sameContentOnDisk = true
		default:
			fh, err := s.cache.getFile(ctx, c.URI)
			if err != nil {
				return nil, err
			}
			_, readErr := fh.Read()
			sameContentOnDisk = readErr == nil && fh.FileIdentity().Hash == hash
		}
		o = &overlay{
			session: s,
			uri:     c.URI,
			version: version,
			text:    text,
			kind:    kind,
			hash:    hash,
			saved:   sameContentOnDisk,
		}
		s.overlays[c.URI] = o
	}

	// Get the overlays for each change while the session's overlay map is
	// locked.
	overlays := make(map[xpath.URI]*overlay)
	for _, c := range changes {
		if o, ok := s.overlays[c.URI]; ok {
			overlays[c.URI] = o
		}
	}
	return overlays, nil
}

func (s *session) GetFile(ctx context.Context, uri xpath.URI) (FileHandle, error) {
	if overlay := s.readOverlay(uri); overlay != nil {
		return overlay, nil
	}
	// Fall back to the cache-level file system.
	return s.cache.getFile(ctx, uri)
}

func (s *session) readOverlay(uri xpath.URI) *overlay {
	s.overlayMu.Lock()
	defer s.overlayMu.Unlock()

	if overlay, ok := s.overlays[uri]; ok {
		return overlay
	}
	return nil
}

func (s *session) Overlays() []Overlay {
	s.overlayMu.Lock()
	defer s.overlayMu.Unlock()

	overlays := make([]Overlay, 0, len(s.overlays))
	for _, overlay := range s.overlays {
		overlays = append(overlays, overlay)
	}
	return overlays
}

func (s *session) GetDiagnostics(ctx context.Context) (map[xpath.URI][]*diagnostic.Diagnostic,error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var start = system.CurrentMillisecond()
	var result = make(map[xpath.URI][]*diagnostic.Diagnostic)
	for uri, _ := range s.overlays {
		var sourceFile = s.program.GetSourceFile(uri)
		if sourceFile != nil {
			result[uri] = append(result[uri], s.program.GetSourceFileSyntacticDiagnostics(sourceFile)...)
			result[uri] = append(result[uri], s.program.GetSourceFileSemanticDiagnostics(sourceFile)...)
		}
	}
	log.Printf("getDiagnostics: %d\n", system.CurrentMillisecond() - start)

	return result, nil
}

type compilerHost struct {
	session_ *session
	ctx context.Context
}

func createCompilerHost(session *session, ctx context.Context) *compilerHost {
	return &compilerHost{
		session_: session,
		ctx:      ctx,
	}
}

func (host *compilerHost) readFile(uri xpath.URI) ([]byte, error){
	handle, err := host.session_.GetFile(host.ctx, uri)
	if err != nil {
		return nil, err
	}
	return handle.Read()
}

func (host* compilerHost) GetProjectFile(uri xpath.URI, onError func(message string)) *ast.ProjectFile {
	//if host.session_.oldModFiles[fileName] != nil {
	//	host.session_.oldModFiles[fileName].ResolvedPackageTree =
	//
	//	SyntacticErrors     []*Diagnostic
	//	ResolvedPackageTree map[*PkgTree]*Package
	//	ResolvedDependents  []*ModFile
	//	IsDefault           bool
	//	return host.session_.oldModFiles[fileName]
	//}

	data, err := host.readFile(uri)
	if err != nil {
		if onError != nil {
			onError(err.Error())
		}
	}
	return parser.ParseProjectFile(uri, data)
}

func (host* compilerHost) GetPackage(uri xpath.URI, project *ast.ProjectFile) *ast.Package {
	var diskPath = uri.Filename()
	if pkg := host.session_.cachePackages[diskPath]; pkg != nil {
		// Need to update the belonging project
		pkg.ProjectFile = project
		return host.session_.cachePackages[diskPath]
	}

	var pkg = new(ast.Package)
	pkg.PackageName = filepath.Base(uri.Filename())
	pkg.Version = project.Version
	pkg.ProjectFile = project
	pkg.DiskPath = uri
	pkg.Files = make(map[string]*ast.SourceFile)
	pkg.Status = ast.PreLoad
	return pkg
}

func (host* compilerHost) GetSourceFile(uri xpath.URI, languageVersion ast.ScriptTarget, onError func(message string)) *ast.SourceFile {
	var fileName = uri.Filename()
	if host.session_.cacheSourceFiles[fileName] != nil {
		return host.session_.cacheSourceFiles[fileName]
	}

	data, err := host.readFile(uri)
	if err != nil {
		if onError != nil {
			onError(err.Error())
		}
	}
	return parser.ParseSourceFile(uri, data, ast.Java8,  ast.MI)
}

func (host* compilerHost) WriteFile(uri xpath.URI, data []byte, onError func(message string)) {

}

func (host* compilerHost) GetCurrentDirectory() string{
	return system.CurrentDirectory()
}

func (host* compilerHost) UseCaseSensitiveFileNames() bool{
	return system.UseCaseSensitiveFileNames()
}

func (host* compilerHost) GetNewLine() string {
	return system.NewLine()
}